// ==UserScript==
// @name          Ninja Echo
// @namespace     ninjaecho
// @description   Ninja Echo is a bot for Stack Overflow chat.
// @include       http://chat.stackoverflow.com/rooms/*
// @grant         GM_xmlhttprequest
// ==/UserScript==

if (typeof pageScope == "undefined")
{
	window.addEventListener("message", function (event)
	{
		try
		{
			var data = JSON.parse(event.data);
		}
		catch (e)
		{
			return;
		}
		if (data.message != "request")
		{
			return;
		}
		GM_xmlhttpRequest({ method: data.method, url: data.url, onload: function (response)
		{
			window.postMessage(JSON.stringify({ message: "response", id: data.id, response: response }), "*");
		},
		onerror: function (response)
		{
			window.postMessage(JSON.stringify({ message: "response", id: data.id, response: response }), "*");
		}});
	});
	
	// run the rest of the script in the page.
	var src = "(" + arguments.callee.toString() + ")();";
	var script = document.createElement('script');
	script.setAttribute("type", "text/javascript");
	script.textContent = "var pageScope = true;\n" + src;
	setTimeout(function ()
	{
		document.body.appendChild(script); document.body.removeChild(script);
	}, 0);
	return;
}

// Everything after this point runs in the page.

window.onload = function ()
{
	bot = {
		roomId: /\d+/.exec(location)[0],
		roomName: document.getElementById("roomname").textContent,
		userName: CHAT.user.current().name,
		xhrCallbacks: {},
		nextCallbackId: 0,
		fkey: fkey().fkey,
		interval: 1000,
		time: 0,
		timer: null,
		socket: null,
		isPolling: false,
		admin: 785745,
		startButton: null,
		stopButton: null,
		
		get news()
		{
			return JSON.parse(localStorage.getItem("bot.news." + bot.roomId)) || [];
		},
		
		set news(value)
		{
			localStorage.setItem("bot.news." + bot.roomId, JSON.stringify(value));
		},
		
		get blacklist()
		{
			return JSON.parse(localStorage.getItem("bot.blacklist." + bot.roomId)) || {};
		},
		
		set blacklist(value)
		{
			localStorage.setItem("bot.blacklist." + bot.roomId, JSON.stringify(value));
		},
		
		init: function ()
		{
			var chatButtons = document.getElementById("chat-buttons");
			bot.startButton = document.createElement("button");
			bot.startButton.innerHTML = "start bot";
			bot.startButton.className = "button";
			bot.startButton.style.display = "inline-block";
			bot.startButton.addEventListener("click", bot.start, false);
			chatButtons.appendChild(bot.startButton);
			bot.stopButton = document.createElement("button");
			bot.stopButton.innerHTML = "stop bot";
			bot.stopButton.className = "button";
			bot.stopButton.style.display = "none";
			bot.stopButton.addEventListener("click", bot.stop, false);
			chatButtons.appendChild(bot.stopButton);
			window.addEventListener("message", function (event)
			{
				try
				{
					var data = JSON.parse(event.data);
				}
				catch (e)
				{
					return;
				}
				if (data.message != "response")
				{
					return;
				}
				bot.xhrCallbacks[data.id](data.response);
			});
		},
		
		start: function ()
		{
			bot.xhr("POST", "/chats/" + bot.roomId + "/events", "since=0&mode=Messages&msgCount=0&fkey=" + bot.fkey, bot.onStart, true);
			bot.startButton.style.display = "none";
			bot.stopButton.style.display = "inline-block";
		},
		
		onStart: function (response, retry)
		{
			if (response.status != 200)
			{
				console.log("Starting failed with status " + response.status);
				return;
			}
			var responseData = JSON.parse(response.responseText);
			bot.time = responseData.time;
			bot.xhr("POST", "/ws-auth", "roomId=" + bot.roomId + "&fkey=" + bot.fkey, bot.onWsauth, true);
		},
		
		onWsauth: function (response, retry)
		{
			if (response.status != 200)
			{
				console.log("Getting WebSocket failed with status " + response.status + ". Falling back to polling.");
				bot.timer = setInterval(bot.poll, bot.interval);
				bot.isPolling = true;
				return;
			}
			if (!("WebSocket" in window))
			{
				console.log("Browser does not support WebSocket. Falling back to polling.");
				bot.timer = setInterval(bot.poll, bot.interval);
				bot.isPolling = true;
				return;
			}
			var responseData = JSON.parse(response.responseText);
			bot.socket = new WebSocket(responseData.url + "?l=" + bot.time);
			bot.socket.onmessage = bot.onSocketMessage;
			bot.socket.onopen = bot.onSocketReadyStateChanged;
			bot.socket.onclose = bot.onSocketReadyStateChanged;
		},
		
		onSocketReadyStateChanged: function (event)
		{
			if (bot.socket.readyState != 1)
			{
				console.log("WebSocket could not be opened. Falling back to polling.");
				bot.timer = setInterval(bot.poll, bot.interval);
				bot.isPolling = true;
			}
			bot.socket.onopen = null;
			bot.socket.onclose = null;
		},
		
		onSocketMessage: function (message)
		{
			bot.onEvent(JSON.parse(message.data));
		},
		
		poll: function ()
		{
			bot.xhr("POST", "/events", "r" + bot.roomId + "=" + bot.time + "&fkey=" + bot.fkey, bot.onPoll, true);
		},
		
		onPoll: function (response, retry)
		{
			if (response.status != 200)
			{
				console.log("Polling failed with status " + status);
				return;
			}
			bot.onEvent(JSON.parse(response.responseText));
		},
		
		onEvent: function (response)
		{
			var room = response["r" + bot.roomId];
			if ("t" in room)
			{
				bot.time = room.t;
			}
			if ("e" in room)
			{
				for (var i = 0; i < room.e.length; i++)
				{
					var event = room.e[i];
					switch (event.event_type)
					{
						case 1:
							bot.onMessage(event);
							break;
						case 8:
							bot.onPing(event);
							break;
					}
				}
			}
		},
		
		onMessage: function (event)
		{
			if (!bot.messageFilter(event))
			{
				return;
			}
			for (var i = 0; i < bot.commands.length; i++)
			{
				var match = bot.commands[i].pattern.exec(event.content);
				if (match)
				{
					bot.commands[i].callback(event, match);
					break;
				}
			}
		},
		
		onPing: function (event)
		{
			bot.xhr("POST", "/messages/ack", "id=" + event.message_id + "&fkey=" + bot.fkey, function () { }, true);
		},
		
		messageFilter: function (event)
		{
			if (event.user_id in bot.blacklist)
			{
				return false;
			}
			var found = false;
			var regex = new RegExp("^\\s*@" + normalizeUserName(bot.userName) + "\\s+", "i");
			var replacement = function ()
			{
				found = true; // regex matches, pass the message to command searching
				return ""; // and strip the ping
			};
			event.content = event.content.replace(regex, replacement);
			return found;
		},
		
		send: function (message)
		{
			bot.xhr("POST", "/chats/" + bot.roomId + "/messages/new", "text=" + encodeURIComponent(message) + "&fkey=" + bot.fkey, bot.onSend, true)
		},
		
		onSend: function (response, retry)
		{
			if (response.status == 409)
			{
				setTimeout(retry, parseInt(/\d+/.exec(response.responseText)[0]) * 1000);
				return;
			}
			else if (response.status != 200)
			{
				console.log("Sending failed with status " + response.status);
				return;
			}
		},
		
		stop: function ()
		{
			if (bot.isPolling)
			{
				clearInterval(bot.timer);
			}
			else
			{
				bot.socket.close();
			}
			bot.startButton.style.display = "inline-block";
			bot.stopButton.style.display = "none";
		},
		
		xhr: function (method, url, data, callback, sameOrigin)
		{
			var retry = function () { bot.xhr(method, url, data, callback, sameOrigin); };
			if (sameOrigin)
			{
				var req = new XMLHttpRequest();
				req.onreadystatechange = function ()
				{
					if (req.readyState == 4)
					{
						callback(req, retry);
					}
				};
				req.open(method, url, true);
				req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
				req.send(data);
			}
			else
			{
				var callbackId = bot.nextCallbackId++;
				window.postMessage(JSON.stringify({ message: "request", method: method, url: url, headers: { "Content-Type": "application/x-www-form-urlencoded" }, id: callbackId }), "*");
				bot.xhrCallbacks[callbackId] = function (response)
				{
					callback(response, retry);
					delete bot.xhrCallbacks[callbackId];
				};
			}
		},
		
		markdown: function (message)
		{
			var replace = [
				{ from: /^<div.*?>|<\/div>$/g, to: "" },
				{ from: /\s?<br\s?\/?>\s?/g, to: "\n" },
				{ from: /^<pre.*?>|<\/pre>$/g, to: "" }, // this doesn't preserve code blocks
				{ from: /<a.*?>|<\/a>/g, to: "" }, // this doesn't preserve named links
				{ from: /<\/?b>/g, to: "**" },
				{ from: /<\/?i>/g, to: "*" },
				{ from: /<\/?code>/g, to: "`" },
				{ from: /<\/?strike>/g, to: "---" },
				{ from: /&quot;/g, to: "\"" },
				{ from: /&amp;/g, to: "&" },
				{ from: /&#39;/g, to: "'" },
				{ from: /&lt;/g, to: "<" },
				{ from: /&gt;/g, to: ">" }
			];
			for (var i = 0; i < replace.length; i++)
			{
				message = message.replace(replace[i].from, replace[i].to);
			}
			return message;
		},
		
		text: function (message)
		{
			var replace = [
				{ from: /<[^>]*>/g, to: "" },
				{ from: /&quot;/g, to: "\"" },
				{ from: /&amp;/g, to: "&" },
				{ from: /&#39;/g, to: "'" },
				{ from: /&lt;/g, to: "<" },
				{ from: /&gt;/g, to: ">" }
			];
			for (var i = 0; i < replace.length; i++)
			{
				message = message.replace(replace[i].from, replace[i].to);
			}
			return message;
		},

		user: function (idOrName)
		{
			if (idOrName in users)
			{
				return users[idOrName];
			}
			idOrName = normalizeUserName(idOrName);
			for (var id in users)
			{
				if (normalizeUserName(users[id].name) == idOrName)
				{
					return users[id];
				}
			}
		},
		
		commands: [
			{
				pattern: /^\s*(?:search(?:\s+for)?|look\s+up|find)\s+(.+?)\s*[.?!]?\s*$/i,
				callback: function (event, match)
				{
					var search = encodeURIComponent(bot.text(match[1])).replace(/%20/g, "+");
					var wikipediaSearch = "http://en.wikipedia.org/w/index.php?search=" + search;
					bot.xhr("GET", wikipediaSearch, null, function (response, retry)
					{
						if (response.finalUrl != wikipediaSearch)
						{
							bot.send(response.finalUrl);
						}
						else
						{
							bot.send("http://google.com/search?q=" + search);
						}
					}, false);
				}
			},
			{
				pattern: /^\s*introduce\s+@?(.+?)\s*[.?!]?\s*$/i,
				callback: function (event, match)
				{
					var user = bot.user(match[1]);
					var name = user !== undefined ? user.name : match[1];
					bot.send("@" + name.replace(/\s/, "") + " Welcome to **" + bot.roomName + "**! Please read [the FAQ](http://chat.stackoverflow.com/faq).");
				}
			},
			{
				pattern: /^\$b(?:lack(?:list)?)? (\d+)/i,
				callback: function (event, match)
				{
					if (event.user_id == bot.admin && match[1] != bot.admin.toString())
					{
						var blacklist = bot.blacklist;
						blacklist[match[1]] = true;
						bot.blacklist = blacklist;
					}
				}
			},
			{
				pattern: /^\$w(?:hite(?:list)?)? (\d+)/i,
				callback: function (event, match)
				{
					if (event.user_id == bot.admin)
					{
						var blacklist = bot.blacklist;
						delete blacklist[match[1]];
						bot.blacklist = blacklist;
					}
				}
			},
			{
				pattern: /^\$n(?:ews)?(?: (\d+))?$/i,
				callback: function (event, match)
				{
					var count = parseInt(match[1]);
					if (isNaN(count))
					{
						count = 5;
					}
					if (count > bot.news.length)
					{
						count = bot.news.length;
					}
					if (count < 0)
					{
						count = 0;
					}
					bot.send(bot.news.slice(bot.news.length - count, bot.news.length).join(" ") || "There is no news.");
				}
			},
			{
				pattern: /^\$n(?:ews)? (.+)/i,
				callback: function (event, match)
				{
					var news = bot.news;
					news.push(bot.markdown(match[1]));
					while (news.length > 100)
					{
						news.shift();
					}
					bot.news = news;
				}
			},
			{
				pattern: /^\s*wh(?:o|at)\s+are\s+you\s*[.?!]?\s*$/i,
				callback: function (event, match)
				{
					bot.send("I am a bot who goes by the name [Ninja Echo](http://ninjaecho.codeplex.com).");
				}
			},
			{
				pattern: /^\$f(?:lip)?$/i,
				callback: function (event, match)
				{
					bot.send(Math.random() < 0.5 ? "Heads" : "Tails");
				}
			},
			{
				pattern: /^\s*should\s+[^\s]+\s+(.+?)\s*[.?!]?\s*$/i,
				callback: function (event, match)
				{
					if (Math.random() < 0.02)
					{
						bot.send("I can't believe you're asking a computer to help you with such a simple decision.");
						return;
					}
					var options = bot.text(match[1]).split(/(?:\s*,\s+or\s+|\s*,\s+|\s+or\s+)/gi);
					var sentencify = true;
					if (options.length == 1)
					{
						options = [
							"Of course.",
							"Why not?",
							"That's an excellent idea!",
							"Sure.",
							"I think so.",
							"Definitely.",
							"Of course not.",
							"That would be incredibly stupid.",
							"That's a terrible idea!",
							"I don't think so.",
							"Please don't.",
							"I wouldn't if I was you.",
							"Maybe.",
							"It's up to you.",
							"I don't care.",
							"Maybe you should ask a lawyer."
						];
						sentencify = false;
					}
					else if (Math.random() < 0.1)
					{
						if (options.length == 2)
						{
							options = [
								"Both of them.",
								"Why not do both?",
								"Neither of them.",
								"Don't do either."
							];
						}
						else
						{
							options = [
								"All of them.",
								"There's no reason not to do them all.",
								"None of them.",
								"You shouldn't do any of them."
							];
						}
						sentencify = false;
					}
					var response = options[Math.floor(Math.random() * options.length)];
					if (sentencify)
					{
						response = response.replace(/^\w/i, function (s) { return s.toUpperCase(); }) + ".";
					}
					bot.send(response);
				}
			},
			{
				pattern: /^\s*who\s+is\s+@?(.+?)\s*[.?!]?\s*$/i,
				callback: function (event, match)
				{
					var user = bot.user(match[1]);
					if (user !== undefined)
					{
						bot.send("http://stackoverflow.com/users/" + user.id);
					}
					else
					{
						bot.send("I don't know.");
					}
				}
			}
		]
	};

	bot.init();
	bot.start();
};
